Yup—owner should never hit your own paywall. Right now your Brand Kit gate is only checking “Pro,” not “Owner/Admin.” Let’s fix it end-to-end (frontend + API) so owner bypasses always.

I’m giving you full files and a clean Replit prompt.

Replit prompt (paste this)
Implement owner/admin bypass for Brand Kit:

1) Add env: IBRANDBIZ_OWNER_EMAILS (comma-separated emails).
2) Update AuthContext to expose: isOwner, isAdmin, isPro, hasProAccess().
   - isOwner = currentUser.email in IBRANDBIZ_OWNER_EMAILS OR users/{uid}.role === 'owner'
   - isAdmin = users/{uid}.role === 'admin'
   - hasProAccess() = isPro || isOwner || isAdmin

3) Create guard component at src/components/guards/ProGate.tsx:
   - If hasProAccess()-> render children
   - Else render Paywall UI

4) Wrap Brand Kit route/page with <ProGate>.
5) Enforce the same bypass on server:
   - Create util auth in /lib/auth/serverAuth.ts to verify Firebase/Session and compute roles
   - In /api/brandkit/* handlers, require hasProAccess() before proceeding

6) Hotfix (temporary): if process.env.NODE_ENV!=='production' and email in IBRANDBIZ_OWNER_EMAILS, treat as Pro to avoid blocking owner while Stripe webhooks sync.

Run, build, test: log in as owner and confirm Brand Kit opens with no paywall. Test a non-pro account still sees paywall.

1) AuthContext — full replacement

src/contexts/AuthContext.tsx

import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import { onAuthStateChanged, User } from "firebase/auth";
import { auth, db } from "@/lib/firebase";
import { doc, getDoc } from "firebase/firestore";

type Role = "owner" | "admin" | "user";
type UserProfile = {
  role?: Role;
  proUntil?: number; // epoch ms if you're tracking Pro by time
  stripeStatus?: "active" | "trialing" | "past_due" | "canceled" | "none";
};

type AuthContextType = {
  currentUser: User | null;
  loading: boolean;
  profile: UserProfile | null;
  isOwner: boolean;
  isAdmin: boolean;
  isPro: boolean;
  hasProAccess: () => boolean;
};

const AuthContext = createContext<AuthContextType>({
  currentUser: null,
  loading: true,
  profile: null,
  isOwner: false,
  isAdmin: false,
  isPro: false,
  hasProAccess: () => false,
});

const OWNER_EMAILS = (import.meta.env.VITE_IBRANDBIZ_OWNER_EMAILS || "")
  .split(",")
  .map(e => e.trim().toLowerCase())
  .filter(Boolean);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [currentUser, setCurrentUser] = useState<User | null>(null);
  const [profile, setProfile] = useState<UserProfile | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsub = onAuthStateChanged(auth, async (u) => {
      setCurrentUser(u);
      if (!u) {
        setProfile(null);
        setLoading(false);
        return;
      }
      try {
        const snap = await getDoc(doc(db, "users", u.uid));
        setProfile((snap.exists() ? (snap.data() as UserProfile) : {}) || {});
      } catch {
        setProfile({});
      } finally {
        setLoading(false);
      }
    });
    return () => unsub();
  }, []);

  const { isOwner, isAdmin, isPro } = useMemo(() => {
    const email = currentUser?.email?.toLowerCase() || "";
    const role = (profile?.role || "user") as Role;

    const ownerByEmail = email && OWNER_EMAILS.includes(email);
    const ownerByRole = role === "owner";

    const isOwner = ownerByEmail || ownerByRole;
    const isAdmin = role === "admin";

    // Determine Pro:
    const now = Date.now();
    const timePro = profile?.proUntil ? profile.proUntil > now : false;
    const stripePro = profile?.stripeStatus === "active" || profile?.stripeStatus === "trialing";

    const isPro = Boolean(timePro || stripePro);

    return { isOwner, isAdmin, isPro };
  }, [currentUser, profile]);

  const hasProAccess = () => isPro || isOwner || isAdmin;

  const value: AuthContextType = { currentUser, loading, profile, isOwner, isAdmin, isPro, hasProAccess };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export const useAuth = () => useContext(AuthContext);


ENV: add VITE_IBRANDBIZ_OWNER_EMAILS="you@yourdomain.com,second@yourdomain.com"

2) ProGate — gate component

src/components/guards/ProGate.tsx

import React from "react";
import { useAuth } from "@/contexts/AuthContext";
import { Button } from "@/components/ui/button";
import { Lock } from "lucide-react";

type Props = { children: React.ReactNode; feature?: string };

export default function ProGate({ children, feature = "this feature" }: Props) {
  const { loading, hasProAccess } = useAuth();

  if (loading) return null;
  if (hasProAccess()) return <>{children}</>;

  return (
    <div className="border rounded-lg p-8 text-center bg-muted/30">
      <Lock className="w-6 h-6 mx-auto mb-3 opacity-70" />
      <h3 className="text-lg font-semibold mb-2">Pro required</h3>
      <p className="text-sm text-muted-foreground mb-4">
        You need <span className="font-medium">Pro</span> to access {feature}.
      </p>
      <div className="flex gap-2 justify-center">
        <Button onClick={() => (window.location.href = "/pricing")}>See plans</Button>
        <Button variant="outline" onClick={() => (window.location.href = "/account")}>
          Already Pro? Refresh
        </Button>
      </div>
    </div>
  );
}

3) Wrap the Brand Kit page

src/pages/BrandKit/BrandKitPage.tsx (top-level wrap)

import React from "react";
import ProGate from "@/components/guards/ProGate";

// ... your other imports

export default function BrandKitPage() {
  return (
    <ProGate feature="Brand Kit">
      {/* existing Brand Kit content goes here unchanged */}
      <div className="space-y-6">
        {/* ... all your Brand Kit UI ... */}
      </div>
    </ProGate>
  );
}


Now owner/admin/pro get in. Free users see the paywall component.

4) Server-side guard (API)

Create shared server auth helper so the backend enforces the same rule.

src/lib/auth/serverAuth.ts

import type { NextApiRequest } from "next";
import * as admin from "firebase-admin";

const OWNER_EMAILS = (process.env.IBRANDBIZ_OWNER_EMAILS || "")
  .split(",")
  .map(e => e.trim().toLowerCase())
  .filter(Boolean);

if (!admin.apps.length) {
  admin.initializeApp({
    credential: admin.credential.applicationDefault(),
  });
}

export async function getServerUser(req: NextApiRequest) {
  const authHeader = req.headers.authorization || "";
  const idToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : undefined;
  if (!idToken) return null;
  try {
    const decoded = await admin.auth().verifyIdToken(idToken);
    return decoded; // { uid, email, ...customClaims }
  } catch {
    return null;
  }
}

export type ServerAccess = {
  uid?: string;
  email?: string;
  role?: "owner" | "admin" | "user";
  isOwner: boolean;
  isAdmin: boolean;
  isPro: boolean;
  hasProAccess: boolean;
};

export async function resolveServerAccess(req: NextApiRequest): Promise<ServerAccess> {
  const u = await getServerUser(req);
  if (!u) return { isOwner: false, isAdmin: false, isPro: false, hasProAccess: false };

  const email = (u.email || "").toLowerCase();

  const userDoc = await admin.firestore().doc(`users/${u.uid}`).get();
  const role = (userDoc.exists ? (userDoc.data()?.role as "owner" | "admin" | "user") : "user") || "user";
  const stripeStatus = userDoc.exists ? userDoc.data()?.stripeStatus : "none";
  const proUntil = userDoc.exists ? Number(userDoc.data()?.proUntil || 0) : 0;

  const now = Date.now();
  const isOwner = OWNER_EMAILS.includes(email) || role === "owner";
  const isAdmin = role === "admin";
  const isPro = stripeStatus === "active" || stripeStatus === "trialing" || proUntil > now;

  return {
    uid: u.uid,
    email,
    role,
    isOwner,
    isAdmin,
    isPro,
    hasProAccess: Boolean(isPro || isOwner || isAdmin),
  };
}


Example Brand Kit API using the guard:

src/pages/api/brandkit/generate.ts

import type { NextApiRequest, NextApiResponse } from "next";
import { resolveServerAccess } from "@/lib/auth/serverAuth";

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== "POST") return res.status(405).json({ error: "Method not allowed" });

  const access = await resolveServerAccess(req);
  if (!access.hasProAccess) {
    return res.status(402).json({ error: "Payment required or owner/admin access missing" });
  }

  // ... your Brand Kit generation logic ...
  return res.json({ ok: true });
}


This stops any sneaky client from skipping the UI guard.

5) Quick “it works right now” hotfix (optional)

If Stripe/webhooks aren’t wired yet and you just need in now:

Add your email to both envs:

Frontend: VITE_IBRANDBIZ_OWNER_EMAILS

Server: IBRANDBIZ_OWNER_EMAILS

Redeploy / restart.

Log out/in. You’ll bypass the wall.

Sanity checklist (do these now)

 Set envs with your email.

 Confirm users/{yourUid}.role if you prefer role-based control.

 Wrap Brand Kit with <ProGate>.

 Call a Brand Kit API and see 200 as owner; non-pro user should get 402.